/*******************************************************
 *
 *  Switch Routines for EECE 494, Assignment 2.
 *
 *  Created by _________, University of British Columbia
 *  Jordan Chin 12558078
 *  Ritwik Chowdhury 71513063
 *  John de Vera 17728072
 *  Dennis Tsang 69972065
 *
 *  This is where you will put your routines to implement
 *  the switch routines.  You should use the headers as
 *  supplied.  All your code for Assignment 2 will go in this
 *  file (and possibly in switch.h)
 *
 ******************************************************/

#include "defs.h"

#define GET_NEXT(cur) ((cur+1) % NUM_PORTS)

// Packet in a queue
struct queue_item
{
	packet_t *packet;
	struct queue_item *next;
};
typedef struct queue_item queue_item_t;

// Queue data structure
typedef struct
{
	pthread_mutex_t mutex;
	queue_item_t *head;
	queue_item_t *tail;
} queue_t;

// Lock a queue
void queue_lock(queue_t *queue)
{
   int status;

   status = pthread_mutex_lock(&(queue->mutex));   
   if (status != 0) {
      printf("Error locking mutex in queue data structure\n");
      exit(0);
   }
}

// Unlock a queue
void queue_unlock(queue_t *queue)
{
   int status;

   status = pthread_mutex_unlock(&(queue->mutex));   
   if (status != 0) {
      printf("Error unlocking mutex in queue data structure\n");
      exit(0);
   }
}

// Push a new packet onto the end of a queue
void queue_push(queue_t *queue, packet_t *packet)
{
	queue_lock(queue);
	
	// Encapsulate packet in a linked list item
	queue_item_t *item = (queue_item_t*) malloc(sizeof(queue_item_t));
	item->packet = packet;
	item->next = NULL;
	
	// Add it to the tail
	if(queue->tail == NULL)
	{
		queue->head = item;
		queue->tail = item;
	}
	else
	{
		queue->tail->next = item;
		queue->tail = item;
	}
	
	queue_unlock(queue);
}

// Pop the first packet off the head of the queue
packet_t *queue_pop(queue_t *queue)
{
	queue_lock(queue);
	
	// Check whether queue is empty
	if(queue->head == NULL)
	{
		return NULL;
	}
	
	// Get the head item
	queue_item_t *head = queue->head;
	
	// Set the new head/tail
	if(head->next == NULL)
	{
		queue->head = NULL;
		queue->tail = NULL;
	}
	else
	{
		queue->head = head->next;
	}
	
	queue_unlock(queue);
	packet_t *packet = head->packet;
	free(head);
	return packet;
}

// Peek at the first packet from the head of the queue
packet_t *queue_peek(queue_t *queue)
{
	queue_lock(queue);
	
	// Check whether queue is empty
	if(queue->head == NULL)
	{
		return NULL;
	}
	
	// Get the head item
	queue_item_t *head = queue->head;
	queue_unlock(queue);
	return head->packet;
}

// Check if a queue is empty
int queue_empty(queue_t *queue)
{
	queue_lock(queue);
	if(queue->head == NULL)
	{
		queue_unlock(queue);
		return TRUE;
	}
	queue_unlock(queue);
	return FALSE;
}

// Input/output buffers
queue_t in_queue[NUM_PORTS];

// Input threads
void *inFunc(void * args)
{
	int i = (int) args;

	while(!die)
	{
		port_lock(&in_port[i]);

		if(in_port[i].flag)
		{
			// Copy packet
			packet_t *packet = (packet_t*) malloc(sizeof(packet_t));
			packet_copy(&in_port[i].packet, packet);
			in_port[i].flag = FALSE;
			port_unlock(&in_port[i]);
			
			// Add packet to processing queue.
			queue_push(&in_queue[i], packet);
		}
		else
		{
			port_unlock(&in_port[i]);
			usleep(1000);
		}
	}
}

// Thread to sort packets to output queues
void *procFunc(void *args)
{
	int pos = 0;
	int itr = 0;

	while(!die)
	{
		int work = 0;
		int rr = FALSE;
		for(itr = 0; itr < NUM_PORTS; itr++)
		{
			int cur = (itr+pos) % NUM_PORTS;
			if(!queue_empty(&in_queue[cur]))
			{
				packet_t *peek = queue_peek(&in_queue[cur]);
				int dst_port = cam_lookup_address(&(peek->address));

				port_lock(&out_port[dst_port]);
				if(!out_port[dst_port].flag)
				{
					packet_t *packet = queue_pop(&in_queue[cur]);
					// Copy packet
					packet_copy(packet, &out_port[dst_port].packet);
					out_port[dst_port].flag = TRUE;
					
					free(packet);
					work++;
				}
				else
				{
					if(!rr)
					{
						// increment the round robin counter
						pos = GET_NEXT(pos);
						rr = TRUE;
					}
				}
				port_unlock(&out_port[dst_port]);
			}
		}

		usleep(1000);
	}
}


void *switch_thread_routine(void *arg)
{
   /* Add your code here */
   
	// Input threads
	pthread_t in_port_threads[NUM_PORTS];
	int i;
	for (i = 0; i < NUM_PORTS; i++)
	{
		pthread_create(&in_port_threads[i], NULL, inFunc, (void *) i);	
	}
	
	// Processing thread
	pthread_t proc_thread;
	pthread_create(&proc_thread, NULL, procFunc, NULL);
}

void switch_init()
{
   /* Add your code here.  At the very least, you probably
      want to include a call to cam_init() */
	cam_init();
}

void switch_add_entry(ip_address_t *address,
                      int port)
{
   /* Add your code here.  It might be as simple as adding
      a call to cam_add_entry() */
	cam_add_entry(address, port);
}

void switch_free()
{
   /* add your code (if any) here */
}
